<?php


namespace EchoPHP\Database\Connections;

use EchoPHP\Database\DetectsLostConnections;
use EchoPHP\Database\Query\Grammar\Grammar;
use EchoPHP\Database\Transaction\ManageTransactions;
use EchoPHP\Database\TuxedoDriver\Tuxedo;
use EchoPHP\Database\TuxedoDriver\TuxedoStatement;
use EchoPHP\Database\TuxedoDriver\TuxedoTransactionStatement;
use EchoPHP\Facades\Log;
use EchoPHP\Support\Arr;
use PDO;
use Closure;
use Exception;
use EchoPHP\Database\Exceptions\QueryException;

abstract class Connection {

    use DetectsLostConnections, ManageTransactions;

    /**
     * PDO连接实例
     * @var \PDO|\Closure|Tuxedo
     */
    protected $connector;

    /**
     * 连接的数据库名称
     * @var string
     */
    protected $database;

    /**
     * 连接配置项
     * @var array
     */
    protected $config = [];

    /**
     * 语法解析器
     * @var Grammar
     */
    protected $queryGrammar;

    /**
     * 数据遍历模式，默认对象形式
     * @var int
     */
    protected $fetchMode = PDO::FETCH_ASSOC;

    public function __construct($connector, $database = '', array $config = []) {
        $this->connector = $connector;
        //一般情况下，数据库已经作为连接属性在数据库连接中使用，本身不会操作，但某些反射形命令中，数据库名还是需要的，比如检查表是否存在,所以先记录下来
        $this->database = $database;
        $this->config = $config;

        $this->queryGrammar = $this->getDefaultQueryGrammar();
    }

    /**
     * 获取默认语法解析器
     * @return Grammar
     */
    protected function getDefaultQueryGrammar() {
        return new Grammar();
    }

    /**
     * 关闭数据库连接
     */
    public function disconnect() {
        $this->setConnector(null);
    }

    /**
     * 拼接分页查询语句
     * @param string $query
     * @param array $bindings
     * @param int $rowNumFrom
     * @param int $rowNumTo
     * @return string
     */
    protected function compileSelectLimit(string $query, array &$bindings, int $rowNumFrom = 0, int $rowNumTo = 0) {
        $rowNumFrom = $rowNumFrom >= 0 ? $rowNumFrom : 0;
        $rowNumTo = $rowNumTo >= 0 ? $rowNumTo : 0;

        if ($rowNumFrom == $rowNumTo && $rowNumFrom == 0) {
            return $query;
        }
        if ($rowNumFrom >= $rowNumTo) {
            throw new \InvalidArgumentException('分页查询起始结束值不能相同');
        }
        return $this->queryGrammar->compileSelectLimit($query, $rowNumFrom, $rowNumTo, $bindings);
    }

    /**
     * 执行查询语句
     * @param string $query
     * @param array $bindings
     * @param int $rowNumFrom
     * @param int $rowNumTo
     * @return array
     */
    public abstract function select(string $query, array $bindings = [], int $rowNumFrom = 0, int $rowNumTo = 0);

    /**
     * 执行查询语句并返回一个数据迭代器
     * @param string $query
     * @param array $bindings
     * @param int $rowNumFrom
     * @param int $rowNumTo
     * @return \Generator
     */
    public abstract function selectCursor(string $query, array $bindings = [], int $rowNumFrom = 0, int $rowNumTo = 0);

    /**
     * 执行查询语句并返回一条结果
     * @param string $query
     * @param array $bindings
     * @return mixed
     */
    public abstract function selectOne(string $query, array $bindings = []);

    /**
     * 执行insert语句
     * @param string $query sql
     * @param array $bindings 绑定变量
     * @return bool 执行结果
     */
    public abstract function insert(string $query, array $bindings = []);

    /**
     * 执行update语句
     * @param string $query
     * @param array $bindings
     * @return int 受影响行数
     */
    public abstract function update(string $query, array $bindings = []);

    /**
     * 执行一条删除语句
     * @param string $query
     * @param array $bindings
     * @return int 受影响行数
     */
    public abstract function delete(string $query, array $bindings = []);

    /**
     * 返回执行语句操作类
     * @param string $query
     * @param array $bindings
     * @return \PDOStatement|TuxedoStatement
     */
    public abstract function prepareStatement(string $query, array $bindings = []);

    /**
     * tuxedo单库事务
     * @param bool $rollbackIfNoAffects
     * @param TuxedoStatement ...$tuxedoStatements
     * @return TuxedoTransactionStatement
     */
    public abstract function prepareTransactionStatement(bool $rollbackIfNoAffects, TuxedoStatement ...$tuxedoStatements);

    /**
     * 执行一条操作语句，update类型操作
     * @param string $query
     * @param array $bindings
     * @return bool 执行失败
     */
    public abstract function execute(string $query, array $bindings = []);

    /**
     *  执行一条操作语句，update类型操作
     * @param string $query
     * @param array $bindings
     * @return mixed
     */
    public abstract function executeWithAffectedRows(string $query, array $bindings = []);

    /**
     * 获取最后插入的id
     * @param null $sequence
     * @return mixed
     */
    public abstract function getLastInsertId($sequence = null);

    /**
     * 获取pdo实例，默认情况下只是定义了pdo实例的匿名函数，尚未建立连接
     * @return \PDO|Tuxedo
     */
    public function getConnector() {
        if ($this->connector instanceof Closure) {
            $this->connector = call_user_func($this->connector);
        }
        return $this->connector;
    }

    /**
     * 设置PDO实例
     * @param $connector
     * @return $this
     */
    public function setConnector($connector) {
        $this->connector = $connector;

        return $this;
    }

    /**
     * 实际sql执行逻辑
     * @param $query
     * @param $bindings
     * @param Closure $callback
     * @return mixed
     * @throws Exception
     */
    protected function run($query, $bindings, Closure $callback) {
        $start = microtime(true);

        $logQuery = '[' . ($this->config['dbtype'] ?? '') . ':' . ($this->config['dbid'] ?? '') . ']' . $query;
        try {
            //这里的回调就是实际的执行逻辑，通过run方法执行可以统一的记录操作执行耗时以及日志记录等
            $result = $callback($query, $bindings);
        } catch (QueryException $e) {
            throw $e;
        } catch (Exception $e) {
            $sqlContext = [
                'query' => $logQuery,
                'bindings' => $bindings,
                'message' => $e->getMessage(),
            ];
            if (method_exists($e, 'getServerName')) {
                $sqlContext['servername'] = $e->getServerName();
                $sqlContext['sendbuffer'] = $e->getSendBuffer();
                $sqlContext['receivebuffer'] = $e->getReceiveBuffer();
            }
            Log::error('SQL执行出现错误', $sqlContext, 'errorsql');
            throw new QueryException($query, $bindings, $e);
        }

        $costTime = round((microtime(true) - $start) * 1000, 2);
        $this->logQuery($logQuery, $bindings, $costTime);

        return $result;
    }

    /**
     * 记录sql执行日志  应用调试或者sql日志模式下，记录sql执行日志  如果定义了慢查询日志，超过定义时间，记录慢查询日志
     * @param $query
     * @param $bindings
     * @param $costTime
     * @throws Exception
     */
    protected function logQuery($query, $bindings, $costTime) {
        if ($this instanceof PdoConnection) {
            Arr::convertCharset($bindings, 'UTF8', 'GBK');
        }

        $slowLogTime = config('app.debug.slowsql', 0);
        if ($costTime > $slowLogTime && $slowLogTime > 0) {
            $sqlContext = [
                'query' => $query,
                'bindings' => $bindings,
                'costtime' => $costTime,
            ];
            Log::warning('SQL执行耗时过长', $sqlContext, 'slowsql');
        }
        $traceSql = config('app.debug.tracesql', false);
        if ($traceSql) {
            $sqlContext = [
                'query' => $query,
                'bindings' => $bindings,
                'costtime' => $costTime,
            ];
            Log::debug('trace sql info', $sqlContext, 'tracesql');
        }
    }

    /**
     * 绑定变量预处理
     * @param array $bindings
     * @return array
     */
    public function prepareBindings(array $bindings) {
        return $bindings;
    }
}